/** 迷你日志类，格式化采用fmtlib库实现
 *      设置 NLOG_SOURCE 可禁用在日志中输出原文件名和函数及行号
 *      设置 NLOG 可禁用日志
 * @file log.hpp
 * @author Kiven Lee
 * @version 1.0
 * @date 2021-08-26
 *
 * @copyright Kivensoft (c) 2018 - 2021
 *
 */
#pragma once

#include <cstring>
#include <cstdio>
#include <ctime>
#include <string>
#include <string_view>
#include <utility>
#include "printf.hpp"

// #define FMT_TIME "{:%Y-%m-%d %H:%M:%S}"

#ifdef NLOG
#   define LOG_LOG(...) ((void)0)
#   define LOG_HEX(...) ((void)0)
#   define LOG_DUMP(...) ((void)0)
#else
#   ifndef NLOG_SOURCE
#       define LOG_LOG(LEVEL, ...) klib::log_t::log(__FILE__, __func__, __LINE__, LEVEL, __VA_ARGS__)
#       define LOG_HEX(LEVEL, ARG, FN, ...) klib::log_t::hex<std::remove_reference<decltype(*ARG)>::type> \
            (__FILE__, __func__, __LINE__, LEVEL, ARG, FN, __VA_ARGS__)
#       define LOG_DUMP(LEVEL, ARG, FN, ...) klib::log_t::dump<std::remove_reference<decltype(*ARG)>::type> \
            (__FILE__, __func__, __LINE__, LEVEL, ARG, FN, __VA_ARGS__)
#   else
#       define LOG_LOG(LEVEL, ...) klib::log_t::log(LEVEL, __VA_ARGS__)
#       define LOG_HEX(LEVEL, ARG, FN, ...) klib::log_t::hex<std::remove_reference<decltype(*ARG)>::type>(LEVEL, ARG, FN, __VA_ARGS__)
#       define LOG_DUMP(LEVEL, ARG, FN, ...) klib::log_t::dump<std::remove_reference<decltype(*ARG)>::type>(LEVEL, ARG, FN, __VA_ARGS__)
#   endif
#endif

#define LOG_TRACE(...) LOG_LOG(klib::log_t::level_t::trace, __VA_ARGS__)
#define LOG_DEBUG(...) LOG_LOG(klib::log_t::level_t::debug, __VA_ARGS__)
#define LOG_INFO(...)  LOG_LOG(klib::log_t::level_t::info , __VA_ARGS__)
#define LOG_WARN(...)  LOG_LOG(klib::log_t::level_t::warn , __VA_ARGS__)
#define LOG_ERROR(...) LOG_LOG(klib::log_t::level_t::error, __VA_ARGS__)

#define LOG_DUMPT(...) LOG_DUMP(klib::log_t::level_t::trace, __VA_ARGS__)
#define LOG_DUMPD(...) LOG_DUMP(klib::log_t::level_t::debug, __VA_ARGS__)
#define LOG_DUMPI(...) LOG_DUMP(klib::log_t::level_t::info, __VA_ARGS__)
#define LOG_DUMPW(...) LOG_DUMP(klib::log_t::level_t::warn, __VA_ARGS__)
#define LOG_DUMPE(...) LOG_DUMP(klib::log_t::level_t::error, __VA_ARGS__)

#define LOG_HEXT(...) LOG_HEX(klib::log_t::level_t::trace, __VA_ARGS__)
#define LOG_HEXD(...) LOG_HEX(klib::log_t::level_t::debug, __VA_ARGS__)
#define LOG_HEXI(...) LOG_HEX(klib::log_t::level_t::info, __VA_ARGS__)
#define LOG_HEXW(...) LOG_HEX(klib::log_t::level_t::warn, __VA_ARGS__)
#define LOG_HEXE(...) LOG_HEX(klib::log_t::level_t::error, __VA_ARGS__)


namespace klib {

    namespace datetime {
        inline tm* localtime_r(time_t* pt, tm* ptm) {
            #ifdef _WIN32
                localtime_s(ptm, pt);
                return ptm;
            #else
                return localtime_r(pt, ptm);
            #endif
        }

        /** 获取当前时间的本地时间表达方式 */
        inline std::tm* now(tm* ptm) {
            time_t t;
            std::time(&t);
            return localtime_r(&t, ptm);
        }

        /** 获取本地时间的YYYY-mm-dd HH:MM:SS字符串格式
         *
         * @param buf 写入的目标缓冲区
         * @param size 目标缓冲区大小
         * @return const char* buf地址
         */
        inline const char* tm_tostring(char* dst, size_t size, tm* src) {
            tm t, *pt = src ? src : now(&t);
            klib::snprintf(dst, size,
                    R"("%04d-%02d-%02d %02d:%02d:%02d")",
                    1900 + pt->tm_year, 1 + pt->tm_mon, pt->tm_mday,
                    pt->tm_hour, pt->tm_min, pt->tm_sec);
            return dst;
        }

        /** 获取时间的本地时间YYYY-mm-dd HH:MM:SS字符串格式
         *
         * @param buf 写入的目标缓冲区
         * @param size 目标缓冲区大小
         * @return const char* buf地址
         */
        inline const char* time_tostring(char* dst, size_t size, time_t src) {
            tm t;
            localtime_r(&src, &t);
            return tm_tostring(dst, size, &t);
        }
    }

    //------------------- Log ---------------------------------

    /** 日志类，只提供静态函数 */
    class log_t {

    public:
        /** 日志级别, 优先级从低到高 */
        enum class level_t { trace, debug, info, warn, error, off };

        typedef std::string_view (*input_t) (void* arg);
        typedef void (*output_t) (void* arg, const char* data, size_t size);

        /** 获取日志级别 */
        static inline level_t get_level() noexcept { return _level; }

        /** 设置日志级别 */
        static inline void set_level(level_t level) noexcept { _level = level; };

        /** 设置日志级别, level: [trace/debug/info/warn/error/off] */
        static inline void set_level(const std::string_view& level) noexcept {
            #ifndef NLOG
                char c = level.empty() ? '\0' : level[0];
                switch (c <= 90 ? c + 32 : c) {
                    case 'd': _level = level_t::debug; break;
                    case 'e': _level = level_t::error; break;
                    case 'i': _level = level_t::info; break;
                    case 'o': _level = level_t::off; break;
                    case 't': _level = level_t::trace; break;
                    case 'w': _level = level_t::warn; break;
                    default: _level = level_t::info;
                }
            #endif
        }

        /** 判断指定的日志级别是否开启 */
        static inline bool is_enabled(level_t level) noexcept {
            return level >= _level && _level != level_t::off;
        }

        /** 判断跟踪级别日志是否开启 */
        static inline bool is_trace_enabled() noexcept { return is_enabled(level_t::trace); }

        /** 判断调试级别日志是否开启 */
        static inline bool is_debug_enabled() noexcept { return is_enabled(level_t::debug); }

        /** 关闭日志在控制台输出 */
        static inline void disable_console(bool v = true) noexcept { _consoleDisable = v; }

        /** 设置日志文件名和最大长度
         *
         * @param name   日志文件名
         * @param maxlen 允许的最大日志文件长度, 超过该长度将备份并重写
         * @return true: 成功, false: 失败
         */
        static inline bool set_file(std::string&& name, uint32_t maxlen) {
            #ifndef NLOG
                if (name.empty()) return true;

                if (_logFP != nullptr)
                    return _perror("log file [%s] is opened, setLogFile fail!\n", _logName.c_str());

                _logFP = fopen(name.c_str(), "rb+");
                if (_logFP == nullptr) {
                    _logFP = fopen(name.c_str(), "wb");
                    if (_logFP == nullptr)
                        return _perror("log file [%s] open fail!\n", name.c_str());
                }

                std::fseek(_logFP, 0, SEEK_END);
                _logPosition = std::ftell(_logFP);
                _logMaxLen = maxlen;
                _logName = name;

                std::atexit(close);
            #endif

            return true;
        }

        /** 设置捕获日志输出的回调函数 */
        static inline void set_outout_event(void* arg, output_t cb) {
            _on_output_arg = arg;
            _on_output = cb;
        }

        /** 刷新缓存，关闭日志文件 */
        static inline void close() {
            if (_logFP != nullptr) {
                std::fflush(_logFP);
                std::fclose(_logFP);
                _logFP = nullptr;
            }
        }

        // 写入到日志, 通常用于写入自定义内容，如不确定是否需要，请勿调用该函数
        static inline void write(const char* data, size_t len) {
            // 输出到控制台
            if (!_consoleDisable)
                std::fwrite(data, 1, len, stdout);

            // 输出到日志文件
            if (_logFP) {
                // 输出到日志文件之前，先判断日志文件是否到达长度限制而需要备份
                if (_logMaxLen > 0 && _logPosition >= _logMaxLen)
                    _truncate();

                // 写入日志文件，需要跳过ansi转义码，ansi转义码为 \x1b[**m 样式，其中**为1-2位数字
                const char* b = (const char*) data, *be = b + len, *p;
                while (b < be && (p = (const char*) std::memchr(b, 0x1b, len))) {
                    _logPosition += std::fwrite(b, 1, p - b, _logFP);
                    b =  p + (p[3] == 'm' ? 4 : 5);
                }
                _logPosition += std::fwrite(b, 1, be - b, _logFP);
            }

            // 输出到用户自定义捕获插件回调函数
            if (_on_output) _on_output(_on_output_arg, data, len);
        }

        static inline void new_line() { write(&_NEW_LINE, 1); }

        /** 输出带源文件信息的日志
         *
         * @param  file_     源文件名称
         * @param  func_     调用函数名称
         * @param  line_     行号
         * @param  level     日志级别
         * @param  fmt       格式化字符串
         * @param  va        不定参数
         */
        static inline void vlog(const char* file_, const char* func_, int line_,
                level_t level, const char* fmt, va_list va) {
            #ifndef NLOG
                if (!is_enabled(level)) return;

                BufIO logio;
                _write_header(file_, func_, line_, (int)level, (void(*)(char, void*)) BufIO::sprintf_out, &logio);
                vfctprintf(BufIO::sprintf_out, &logio, fmt, va);
                logio.push_back('\n');
            #endif
        }

        /** 输出指定级别的日志
         *
         * @param  level     日志级别
         * @param  fmt       格式化字符串
         * @param  va        不定参数
         */
        static inline void vlog(level_t level, char const* fmt, va_list va) {
            #ifndef NLOG
                if (!is_enabled(level)) return;

                BufIO logio;
                _write_header((int) level, (void(*)(char, void*)) BufIO::sprintf_out, &logio);
                vfctprintf(BufIO::sprintf_out, &logio, fmt, va);
                logio.push_back('\n');
            #endif
        }

        /** 输出带源文件信息的日志
         *
         * @param  file_     源文件名称
         * @param  func_     调用函数名称
         * @param  line_     行号
         * @param  level     日志级别
         * @param  fmt       格式化字符串
         */
        static inline void log(const char* file_, const char* func_, int line_,
                level_t level, const char* fmt, ...) {
            #ifndef NLOG
                va_list va;
                va_start(va, fmt);
                vlog(file_, func_, line_, level, fmt, va);
                va_end(va);
            #endif
        }

        /** 输出指定级别的日志
         *
         * @param  level     日志级别
         * @param  fmt       格式化字符串
         */
        static inline void log(level_t level, char const* fmt, ...) {
            #ifndef NLOG
                va_list va;
                va_start(va, fmt);
                vlog(level, fmt, va);
                va_end(va);
            #endif
        }

        #ifndef NLOG
            #define VLOG(NAME) \
                template<typename... ARGS> \
                static inline void NAME(const char* fmt, ARGS&&... args) { \
                    log(level_t::NAME, fmt, std::forward<ARGS>(args)...); \
                }
            #else
            #define VLOG(NAME) static inline void NAME(const char* fmt, ...) {}
        #endif

        VLOG(trace)
        VLOG(debug)
        VLOG(info)
        VLOG(warn)
        VLOG(error)

        #undef VLOG

        /** 输出指定级别的日志(以16进制方式输出，用回调函数获取数据, 一直回调，直到返回的string_view长度为0)
         *
         * @param  file_     源文件名称
         * @param  func_     调用函数名称
         * @param  line_     行号
         * @param  level     日志级别
         * @param  arg       回调函数参数
         * @param  cb        回调函数: std::string_view (T* arg)
         * @param  fmt       格式化字符串
         * @param  args      格式化参数
         */
        template<typename T, typename... ARGS>
        static inline void hex(const char* file_, const char* func_, int line_,
                level_t level, T* arg, std::string_view(*cb)(T*), const char* fmt, ARGS&&... args) {
            _hex(file_, func_, line_, level, (void*)arg, (input_t)cb, fmt, std::forward<ARGS>(args)...);
        }

        /** 输出指定级别的日志(以16进制方式输出，用回调函数获取数据, 一直回调，直到返回的string_view长度为0)
         *
         * @param  level     日志级别
         * @param  arg       回调函数参数
         * @param  cb        回调函数: std::string_view (T* arg)
         * @param  fmt       格式化字符串
         * @param  args      格式化参数
         */
        template<typename T, typename... ARGS>
        static inline void hex(level_t level, T* arg, std::string_view(*cb)(T*), const char* fmt, ARGS&&... args) {
            return _hex(nullptr, nullptr, 0, level, (void*)arg, (input_t)cb, fmt, std::forward<ARGS>(args)...);
        }

        /** 输出指定级别的日志(输出用户自定义数据，用回调函数获取数据, 一直回调，直到返回的string_view长度为0)
         *
         * @param  file_     源文件名称
         * @param  func_     调用函数名称
         * @param  line_     行号
         * @param  level     日志级别
         * @param  arg       回调函数参数
         * @param  cb        回调函数: std::string_view (T* arg)
         * @param  fmt       格式化字符串
         * @param  args      格式化参数
         */
        template<typename T, typename... ARGS>
        static inline void dump(const char* file_, const char* func_, int line_,
                level_t level, T* arg, std::string_view(*cb)(T*), const char* fmt, ARGS&&... args) {
            return _dump(file_, func_, line_, level, (void*)arg, (input_t)cb, fmt, std::forward<ARGS>(args)...);
        }

        /** 输出指定级别的日志(输出用户自定义数据，用回调函数获取数据, 一直回调，直到返回的string_view长度为0)
         *
         * @param  level     日志级别
         * @param  arg       回调函数参数
         * @param  cb        回调函数: std::string_view (T* arg)
         * @param  fmt       格式化字符串
         * @param  args      格式化参数
         */
        template<typename T, typename... ARGS>
        static inline void dump(level_t level, T* arg, std::string_view(*cb)(T*), const char* fmt, ARGS&&... args) {
            return _dump(nullptr, nullptr, 0, level, (void*)arg, (input_t)cb, fmt, std::forward<ARGS>(args)...);
        }

    private:
        static inline bool        _consoleDisable = false;
        static inline level_t     _level          = level_t::info;
        static inline std::string _logName        = "";
        static inline uint32_t    _logMaxLen      = 10485760;
        static inline uint32_t    _logPosition    = 0;
        static inline std::FILE * _logFP          = nullptr;

        static inline void* _on_output_arg = nullptr;
        static inline output_t _on_output = nullptr;

        // 输出错误信息
        static inline bool _perror(const char* fmt, ...) {
            std::fwrite("\x1b[31m", 1, 5, stdout);
            va_list va;
            va_start(va, fmt);
            klib::vprintf(fmt, va);
            va_end(va);
            std::fwrite("\x1b[0m", 1, 4, stdout);
            return false;
        }

        // 切换日志文件(归档当前日志文件，创建新的日志文件)
        static inline void _truncate() {
            char buf[256];
            size_t flen = _logName.length();
            char *bak = flen + 5 > sizeof(buf) ? new char[flen + 5] : buf;

            std::memcpy(bak, _logName.c_str(), flen);
            char *p = bak + flen;
            *p++ = '.'; *p++ = 'b'; *p++ = 'a'; *p++ = 'k'; *p = '\0';

            std::fclose(_logFP);

            // 删除旧的日志备份文件，将现日志文件改名为备份文件，并新建新的日志文件
            std::remove(bak);
            if (!std::rename(_logName.c_str(), bak)) {
                _logFP = fopen(_logName.c_str(), "wb");
                _logPosition = 0;
            } else {
                _perror("can't rename [%s] -> [%s]", _logName.c_str(), bak);
            }

            if (bak != buf) delete []bak;
        }

        // 写入日志头部
        static inline void _write_header(int level, void (*out)(char c, void* arg), void* arg) {
            fctprintf(out, arg, "[%T] %s[%s]\x1b[0m -- ",
                    std::time(nullptr), _LEVEL_COLOR[level], _LEVEL_NAME[level]);
        }

        // 写入日志头部
        static inline void _write_header(const char* file_, const char* func_, int line_,
                int level, void (*out)(char c, void* arg), void* arg) {
            fctprintf(out, arg, "[%T] %s[%s]\x1b[0m [%s:%s:%d] -- ",
                    std::time(nullptr), _LEVEL_COLOR[level], _LEVEL_NAME[level],
                    _onlyfile(file_), func_, line_);
        }

        // 常量计算函数，返回去除路径的文件名
        static inline constexpr const char* _onlyfile(const char* file_) {
            const char* p = file_;
            while (*p) ++p;
            while (p >= file_ && *p != '\\' && *p != '/') --p;
            return p + 1;
        }

        static inline void _hex(const char* file_, const char* func_, int line_,
                level_t level, void* arg, input_t cb, const char* fmt, ...) {
            #ifndef NLOG
                if (!is_enabled(level)) return;

                va_list va;
                va_start(va, fmt);
                if (file_)
                    vlog(file_, func_, line_, level, fmt, va);
                else
                    vlog(level, fmt, va);
                va_end(va);

                HexBufIO lhio;
                for (std::string_view s = cb(arg); s.length(); s = cb(arg))
                    for (size_t i = 0, n = s.length(); i < n; ++i)
                        lhio.push_back(s[i]);
            #endif
        }

        static inline void _dump(const char* file_, const char* func_, int line_,
                level_t level, void* arg, input_t cb, const char* fmt, ...) {
            #ifndef NLOG
                if (!is_enabled(level)) return;

                va_list va;
                va_start(va, fmt);
                if (file_)
                    vlog(file_, func_, line_, level, fmt, va);
                else
                    vlog(level, fmt, va);
                va_end(va);

                for (std::string_view s = cb(arg); s.length(); s = cb(arg))
                    write(s.data(), s.length());

                new_line();
            #endif
        }

        static inline const char _NEW_LINE = '\n';

        // 日志级别
        static inline const char* _LEVEL_NAME[] = {"TRACE", "DEBUG", "INFO ", "WARN ", "ERROR", "OFF  "};

        // 日志颜色
        static inline const char* _LEVEL_COLOR[] = {"\x1b[37m", "\x1b[37m", "\x1b[34m", "\x1b[35m", "\x1b[31m", "\x1b[0m"};

        /** 日志输出缓冲类 */
        struct BufIO {
            char   data[512];
            size_t pos;

            BufIO(): pos(0) {}

            ~BufIO() { if (pos) log_t::write(data, pos); }

            void push_back(const char& c) {
                if (pos >= sizeof(data)) {
                    log_t::write(data, pos);
                    pos = 0;
                }
                data[pos++] = c;
            }

            static inline void sprintf_out(char c, BufIO* arg) {
                arg->push_back(c);
            }
        };

        /** 日志16进制输出缓冲类 */
        struct HexBufIO {
            static inline const char _HEXC[] = "0123456789abcdef";

            char data[128 * 3 + 4];
            size_t  pos;

            HexBufIO(): pos(4) {
                std::memset(data, ' ', sizeof(data));
                for (size_t i = 4; i < sizeof(data); i += 3)
                    data[i] = '\n';
            }

            ~HexBufIO() {
                if (pos) {
                    data[pos - 1] = '\n';
                    log_t::write(data, pos);
                }
            }

            void push_back(const char& v) {
                if (pos >= sizeof(data)) {
                    log_t::write(data, pos);
                    pos = 4;
                }
                data[pos++] = _HEXC[v >> 4];
                data[pos++] = _HEXC[v & 0xf];
                ++pos;
            }

            static inline void sprintf_out(char c, HexBufIO* arg) {
                arg->push_back(c);
            }
        };

    };

} // namespace klib
